本篇同步發文在個人Blog: [讀書筆記] Threading in C# - PART 1: GETTING STARTED
這陣子換了新工作環境,公司使用不少C# Thread相關的技術,而知名書籍C# in a Nutshell的作者Joseph Albahari,將C# Thread的技術教學都免費公開,因此會閱讀他的教學文來撰寫讀書筆記,希望在工作專案或Side Project都有幫助到。
作者有一些程式碼並非完整,我會盡量寫出實際可執行的範例,且有些功能Net Core以後不支援,也會加上註明。以下正式開始。
Thread是獨立的執行路徑, 也能同時和其他Thread工作
C# Client程式(Console, wpf, winform等), CLR都會起單一的Main thread執行
被賦予工作的Thread, 只要那工作(function)完成, 該Thread也就結束,也無法重新工作
每個Thread會分配到記憶體獨立的Stack區塊, 所以function的變數能有地方儲存
Thread如果參考到同一物件, 該物件的資料會共享. 如果用static的資料也一樣是共享
但資料共享容易造成_Thread-Safe_的問題, 要特別處理, 像下面範例, 因為兩個取到done的值都是false, 所以都會執行
using System;
using System.Threading;
class ThreadTestWithSharedData
{
private bool done = false;
static void Main()
{
ThreadTestWithSharedData test = new ThreadTestWithSharedData();
Thread t = new Thread(test.GoMaybeNotSafe);
t.Start();
test.GoMaybeNotSafe();
Console.Read();
}
public void Go()
{
if(!done){
done = true;
Console.WriteLine("done");
}
}
public void GoMaybeNotSafe()
{
if(!done){
Console.WriteLine("done");
done = true;
}
}
}
使用Exclusive lock, 只允許一個thread運算
當Thread被Blocked, 不會消耗CPU資源
使用Join可等待Thread完成
使用Sleep讓當前Thread暫停指定的時間
不管是Join或Sleep, 都是_Blocked_
Thread.Sleep(0) 將目前Thread的運算時間放其, 將CPU時間交給別的Thread, 等同功能是 Thread.Yield()
用Sleep(0)或Yield, 可以用來找thread safety的問題, 假如把Yield填入程式任何地方且出現問題, 代表這程式碼有Bug
在CLR裡有個Thread Scheduler, 代表作業系統, 由它Thread的執行時間
單一處理器的系統, 切的time slice時間比switch context的時間還長
多處理器的系統, 切的time slice有concurrency, 可以同時執行多個thread
Thread如果被preempted(搶占), 代表它是被interrupted, 比如time-slicing
多個Thread可以執行在1個Process
Process之間是互相隔離
Thread之間互相分享Heap記憶體的資料
Maintaining a responsive user interface: 其他的Worker Thread可背後執行消耗的任務, 而Main(UI) Thread與User操作互動
Making efficient use of an otherwise blocked CPU:
Parallel programming: 在多核心/多處理器的環境, 多個執行緒能平行分擔工作
Speculative(投機性) execution: 有些任務可以用多個演算法同時運算, 最終結果取最快運算完的.
Allowing requests to be processed simultaneously: .NET的Server功能(WCF、ASP.NET等) 收到Request, 會自動建立多執行緒來處理. Client也是可同樣的作法.
強調多執行緒之間共用資料時, 都會有Bug的產生. 建議把多執行緒的邏輯能封裝在獨立的library, 也比較好測試
有些功能用太多執行緒不見得更快, 比如Disk IO, 只要幾個thread讀取 比 10幾個thread還快
可以在Thread.Start(someArgs)代入該function的參數
也可以用ParameterizedThreadStart, 但是function的參數必須用object, 再另外轉型
Lambda expressions and captured variables: 傳參數要注意共用性的問題, 下面的輸出可能是0223557799, 而不是0~9各出現一次, 原因是有時多個Thread對i會存取到一樣的
for (int i = 0; i < 10; i++)
new Thread (() => Console.Write (i)).Start();
解決Captured variable的方法是指定變數:
for (int i = 0; i < 10; i++)
{
int temp = i;
new Thread (() => Console.Write (temp)).Start();
}
可以指定Thread的名字, 比較容易做Debug
用Thread.CurrentThread.Name = XXXX 指定名字
Thread預設建立是Foreground, 代表它執行完才會讓App結束
指定Thread.IsBackground = true, App終止時並不會理會Background的thread而強制終止
如果在程式要結束且有finally的background thread, 這thread也會被忽略掉, 解決方法有2
用Join
如果是Pooled thread, 可用event wait handler
Priority決定thread的執行時間長度
小心使用Priority, 否則可能造成對其他thread取資源的starvation
如果Process的Priority很低, 即使調高Thread的Priority也是會被限制資源
Process有個RealTime的Priority, 這會幾乎搶佔所有作業系統的資源, 小心使用, 一般用High就好
如果要做RealTime的應用程式且包含使用者介面, 通常會拆開來, 使用者介面一個程式、後端運算是另一個程式, 彼此溝通用Remoting(WCF, Web Api之類)或memory-mapped files (C# in a Nutshell 有提到!! 沒用過~~)
using System;
using System.Threading;
class ThreadThrowException
{
static void Main(string[] args){
try{
Thread t = new Thread(Go);
t.Start();
}
catch(Exception ex){
Console.WriteLine("Hi i am here" + ex.Message);
}
Console.Read();
}
static void Go()
{
throw new Exception("Null");
}
}
using System;
using System.Threading;
class ThreadThrowException2
{
static void Main(string[] args){
Thread t = new Thread(Go);
t.Start();
Console.Read();
}
static void Go()
{
try{
throw new Exception("Null");
}
catch(Exception ex){
Console.WriteLine("Hi i am here" + ex.Message);
}
}
}
Global的異常事件處理(WPF和Winform的Application.DispatcherUnhandledException和 Application.ThreadException), 只有Main UI thread拋出的異常才會處理, 其他Worker thread的異常要自己處理
AppDomain.CurrentDomain.UnhandledException會被任何異常觸發, 但無法阻止後續程式的中止, 以下範例兩個exception都會被UnhandledException捕捉, 但程式仍直接中止
using System;
using System.Threading;
class ThreadThrowExceptionWithAppDomainHandler
{
static void Main(string[] args){
AppDomain currentDomain = AppDomain.CurrentDomain;
currentDomain.UnhandledException += new UnhandledExceptionEventHandler(MyHandler);
try{
Thread t = new Thread(Go);
t.Start();
}
catch(Exception ex){
Console.WriteLine("Hi i am here" + ex.Message);
}
throw new Exception("TEST");
Console.Read();
}
static void Go()
{
throw new Exception("Null");
}
static void MyHandler(object s, UnhandledExceptionEventArgs args)
{
Exception e = (Exception) args.ExceptionObject;
Console.WriteLine("runtime terminating: {0} ", args.IsTerminating);
}
}
Task Parallel Library
ThreadPool.QueueUserWorkItem
asynchronous delegates (BeginXXXXX...)
BackgroundWorker
WCF, Remoting, ASP.NET, ASMX Web service等的應用程式Server
System.Timers.Timer和System.Threading.Timer
Net有用Async結尾的函式, 比如WebClient(使用event-based asynchronous pattern)和BeginXXXX開頭的函式(asynchronous programming model pattern)
PLINQ
不能對Thread pool設定Name
thread pool都是background thread
block thread pool可能會造成一些潛在問題, 有一些優化的手法(比如ThreadPool.SetMinThreads)
Thread pool設過priority後, 任務執行完回收到pool會賦歸成normal priority
可以用Thread.CurrentThread.IsThreadPoolThread 查看目前Thread是不是從pool來的
新的Task類別使用Thread pool更簡單
非泛型的Task類別取代ThreadPool.QueueUserWorkItem
泛型的Task類別取代asynchronous delegate (BeginXXXXX...)
非泛型的Task類別用Task.Factory.StartNew
會回傳一個Task物件, 可以用Wait()等待, 而Task指定的函式發生Exception時, 會捕捉到
如果不對Task物件做Wait, 而中間發生的Exception會造成程式中止 ( 這個用Console程式無法成功, 主程式沒被中止)
Task的結果可用.Result取得該Task回傳的結果
在Task取Result有Exception時, 會包裝在AggregateException, 沒處理的話會讓程式中止
QueueUserWorkItem
像是new Thread一樣, 代入void的function, 也能代入參數, 都包裝在object
如果function有未處理的exception, 將造成程式中止
using System;
using System.Threading;
class QueueUserWorkItem
{
static void Main(string[] args){
ThreadPool.QueueUserWorkItem(Go);
ThreadPool.QueueUserWorkItem(Go, 12345);
Console.Read();
}
static void Go(object data)
{
Console.WriteLine("Hello " + data);
}
}
Asynchronous delegates
能夠回傳值, 基於IAsyncResult
Asynchronous delegate和asynchronous methods不一樣, 有些函式庫也是用BeginXXX/EndXXX開頭
使用Asynchronous delegates的流程:
建立要被委託的函式, 必需指定成Func類別
用Func的BeginInvoke呼叫該函式, 會回傳IAsyncResult
用Func的EndInvoke代入IAsyncResult變數, 將取得結果
using System;
using System.Threading;
class AsynchronousDelegate
{
static void Main(string[] args){
Func<string, int, string> task = Go;
IAsyncResult cookie = task.BeginInvoke("test", 123, null, null);
string result = task.EndInvoke(cookie);
Console.WriteLine("Result is " + result);
Console.Read();
}
static string Go(string name, int n)
{
return name + " and " + n.ToString();
}
}
如果事情還未完成, 會等它完成
接收回傳值
將Exception拋回至Caller
技術上來講, 如果函式沒有要回傳值, 可以不呼叫EndInvoke, 但內部造成的Exception要小心. 所以建議都呼叫EndInvoke
另一種用法是把處理運算結果寫在另一個委託函式, 該函式接收IAsyncResult的參數. 而不是在Caller呼叫 EndInvoke
using System;
using System.Threading;
class AsynchronousDelegate2
{
static void Main(string[] args){
Func<string, int, string> task = Go;
task.BeginInvoke("test", 123, Done, task);
Console.Read();
}
static void Done(IAsyncResult cookie)
{
var target = (Func<string, int, string>) cookie.AsyncState;
string result = target.EndInvoke(cookie);
Console.WriteLine("Result is " + result);
}
static string Go(string name, int n)
{
return name + " and " + n.ToString();
}
}
ThreadPool.SetMaxThreads可以設置Thread pool最多的Thread數量
每個環境有預設的上限
Framework 4.0 & 32-bit 可設1023個
Framework 4.0 & 64-bit 可設32768個
Framework 3.5 可設每個核心250個
Framework 2.0 可設每個核心25個
ThreadPool.SetMinThreads能設置最小的Thread數量, 預設是每個core會有1個
SetMinThreads能優化的狀況是, 因為建立Thread會有延遲, 但如果SetMinThreads指定X個, 這X個Thread不要有延遲.